﻿Imports System.IO
Imports System.Xml.Serialization
Imports System.Xml
Imports hOOt.Hoot
Imports SharpSvn
Imports LibGit2Sharp

Public Class WikiModel

    Public Shared InvalidChars As Char() = {","c, ";"c, "+"c, "~"c, "^"c, "&"c, "|"c, "\"c, "/"c, "*"c, "."c}

    Private DataDirPath As String

#Region "Singelton"
    Private Shared _Instance As New WikiModel(ReadDataDir())
    Public Shared ReadOnly Property Instance As WikiModel
        Get
            Return _Instance
        End Get
    End Property
    Sub New(DataDirPath As String)
        If _Instance IsNot Nothing Then Throw New Exception("WikiModel cannot be used to create objects, use WikiModel.Instance()")
        Me.DataDirPath = DataDirPath
        Me.PageDb = New PagesDataBase(Path.Combine(DataDirPath, "Pages"))
        Me.Archiver = New PagesArchiver(Path.Combine(DataDirPath, "Archive"), Me.PageDb)
        Me.NameIndex = New PageNameIndex(Path.Combine(DataDirPath, "NameIndex.xml"))
        Me.TagsIndex = New PageTagsIndex(Path.Combine(DataDirPath, "TagIndex.xml"), Path.Combine(DataDirPath, "CategoryList.xml"), NameIndex)
        Me.Searcher = New SearchIndexer(Path.Combine(DataDirPath, "Search Index"), Me.PageDb)
        Me.Backuper = New BackupManager(DataDirPath, DataDirPath & "\..", "WikiData\Pages\")

        DefaultPageType = "מתכון"
        PageTypesTagGroups = New Dictionary(Of String, HashSet(Of String))
        PageTypesTagGroups("מתכון") = New HashSet(Of String) From {"רכיבים", "התעסקות", "מקור", "מוצא", "מועד", "עונה", "ארוחה", "מנה", "קטגוריה", "תגיות"}
        PageTypesTagGroups("ארוחה") = New HashSet(Of String) From {"מרכיבים", "מוצא", "מועד", "עונה", "זמן", "קטגוריה", "תגיות"}
        PageTypesTagGroups("ספר") = New HashSet(Of String) From {"מחבר", "נושא", "קטגוריה", "תגיות"}
        PageTypesTagGroups("מוצר") = New HashSet(Of String) From {"יצרן", "רכיבים", "מוצא", "עונה", "מועד", "קטגוריה", "תגיות"}
        PageTypesTagGroups("צמח") = New HashSet(Of String) From {"מוצא", "עונה", "קטגוריה", "תגיות"}
        PageTypesTagGroups("תג") = New HashSet(Of String) From {"קטגוריה", "תגיות"}
        PageTypesTagGroups("קטגוריה") = New HashSet(Of String) From {"קטגוריה", "תגיות"}
        PageTypesTagGroups("מועד") = New HashSet(Of String) From {"עונה", "קטגוריה", "תגיות"}
        PageTypesTagGroups("ארגון") = New HashSet(Of String) From {"קטגוריה", "תגיות"}
        PageTypesTagGroups("אחר") = New HashSet(Of String) From {"סוג", "קטגוריה", "תגיות"}

    End Sub

#End Region

    Private Property NameIndex As PageNameIndex
    Private Property TagsIndex As PageTagsIndex
    Private Property PageDb As PagesDataBase
    Private Property Archiver As PagesArchiver
    Private Property Searcher As SearchIndexer
    Private Property Backuper As BackupManager

    Public Property DefaultPageType As String
    Public Property PageTypesTagGroups As Dictionary(Of String, HashSet(Of String))

    Private Sub _DoSavePage(updatedPage As FiledWikiPageModel)
        updatedPage.VersionNumber += 1
        updatedPage.DateModeified = Now
        updatedPage.LastAuthor = "שי"

        PageDb.SavePage(updatedPage)
        NameIndex.Update(updatedPage)
        TagsIndex.Update(updatedPage)

        Backuper.update(updatedPage.Title, updatedPage.LastChangeNotes)
    End Sub

    'Create or update a page
    Public Sub SavePage(Page As FiledWikiPageModel)
        Dim exists As Boolean = PageDb.HasPage(Page.Title)
        If exists Then
            Archiver.ArchivePage(Page.Title)
        End If
        Page.Deleted = False
        If Page.Content Is Nothing Then Page.Content = ""
        Page.Content = MarkupTools.ProcessExtraMarkupHtml(Page.Content)
        _DoSavePage(Page)
        Searcher.UpdatePage(Page)
    End Sub

    Public Sub DeletePage(Page As FiledWikiPageModel)
        TagsIndex.Remove(Page)
        NameIndex.Remove(Page)
        Searcher.RemovePage(Page)
        PageDb.DeletePage(Page.Title)
    End Sub

    Public Sub RenamePage(Page As FiledWikiPageModel, NewTitle As String, keepOldTitleAsAlias As Boolean)
        NewTitle = StrConv(NewTitle, VbStrConv.ProperCase)

        'TODO: should rename in archive?

        If HasExactPage(NewTitle) Then
            Throw New WikiException(String.Format("Failed to rename the page '{0}'. There is already a page named {1}", Page.Title, NewTitle))
            Exit Sub
        End If

        Searcher.RemovePage(Page)


        If PageDb.HasPage(Page.Title) Then
            Archiver.ArchivePage(Page.Title) 'Does not delete file, just copies to archive
        End If
        Backuper.move(Page.Title, NewTitle) ' moves the file, but doesn't commit

        NameIndex.Remove(Page)
        TagsIndex.Remove(Page)

        Dim oldTitle As String = Page.Title
        Page.Title = NewTitle
        Page.AddName(NewTitle)

        If Not keepOldTitleAsAlias Then
            Page.RemoveName(oldTitle)
        Else
            Page.AddName(oldTitle) 'Should still be in names collection, but to be sure
        End If
        Page.LastChangeNotes = String.Format("שינוי שם הדף מ- '{0}' ל- '{1}'", oldTitle, NewTitle)

        _DoSavePage(Page) 'saves the updated file in it's new name and updates. also commits

        Searcher.UpdatePage(Page)

    End Sub

    Public Sub RemovePage(Page As FiledWikiPageModel)
        'Not sure this should exist
        DeletePage(Page)
        Archiver.DeleteFromArchive(Page.Title)
    End Sub
    Public Function HasExactPage(PageName As String) As Boolean
        PageName = StrConv(PageName, VbStrConv.ProperCase)
        Dim Pages = NameIndex.GetPages(PageName)
        Return Pages.Contains(PageName)
    End Function

    Public Function HasPage(PageName As String) As Boolean
        PageName = StrConv(PageName, VbStrConv.ProperCase)
        Return NameIndex.HasPages(PageName)
    End Function

    Public Function GetPage(PageName As String, Optional Exact As Boolean = False) As WikiPageModel
        PageName = StrConv(PageName, VbStrConv.ProperCase)

        Try
            Dim Pages = NameIndex.GetPages(PageName)
            If Pages.Count = 0 Or (Exact AndAlso Not Pages.Contains(PageName)) Then
                Throw New WikiNoPageException(PageName)

            ElseIf Exact Then
                Return PageDb.GetPage(PageName)

            ElseIf Pages.Count = 1 Then
                Return PageDb.GetPage(Pages.First())

            Else
                Dim PageNames As New List(Of String)
                For Each Page In Pages
                    PageNames.Add(Page)
                Next
                Return New VirtualDisambiguationPageModel(PageName, PageNames)
            End If

        Catch e As PagesDataBase.PageNotFoundException
            Throw New WikiNoPageException(PageName)
        End Try
    End Function
    Public Function GetExactPage(PageName As String) As FiledWikiPageModel
        Return GetPage(PageName, True)
    End Function
    Public Function GetPageVersion(PageName As String, version As Long) As FiledWikiPageModel
        If version = 0 Then
            Return PageDb.GetPage(PageName)
        End If

        Try
            Return Archiver.GetPage(PageName, version)
        Catch e As WikiNoPageVersionException
            Dim page = PageDb.GetPage(PageName)
            If page.GetVersionCode() = version Then
                Return page
            Else
                Throw New WikiNoPageVersionException(PageName, version)
            End If
        End Try
    End Function

    Public Function GetAllPageTitles() As SerializableSortedSet(Of String)
        Return NameIndex.GetAllPages()
    End Function

    Public Function GetPagesByTag(Tag As String) As SerializableSortedSet(Of String)
        If HasPage(Tag) Then
            Dim TagPage As FiledWikiPageModel = GetPage(Tag)
            Dim TaggedPages As New SerializableSortedSet(Of String)
            For Each OtherTagName In TagPage.Names
                TaggedPages.UnionWith(GetPagesByExactTag(OtherTagName))
            Next
            Return TaggedPages
        Else
            Return GetPagesByExactTag(Tag)
        End If
    End Function

    Public Function GetPagesByExactTag(Tag As String) As SerializableSortedSet(Of String)
        Return TagsIndex.GetPages(Tag)
    End Function

    Public Function Search(Query As String) As SearchResults
        Return Searcher.Search(Query)
    End Function


    Public Function GetPageVersionsDates(PageTitle As String) As List(Of Date)
        Dim versions As List(Of Date) = Archiver.ListVersionsDates(PageTitle)
        If HasExactPage(PageTitle) Then versions.Add(GetExactPage(PageTitle).DateModeified)
        Return versions
    End Function
    Public Function GetPageVersions(PageTitle As String) As List(Of FiledWikiPageModel)
        Dim versions As List(Of FiledWikiPageModel) = Archiver.GetVersions(PageTitle)
        If HasExactPage(PageTitle) Then versions.Add(GetExactPage(PageTitle))
        Return versions
    End Function

    Public Function GetAllTags() As List(Of String)
        Return TagsIndex.GetAllTags()
    End Function

    Public Function GetRecentlyChangedPages(Optional Count As Integer = 20) As List(Of FiledWikiPageModel)
        Dim l As New List(Of FiledWikiPageModel)
        For Each p In PageDb.GetRecentlyChangedPages(Count)
            l.Add(GetPage(p, True))
        Next
        Return l
    End Function


    Public Shared Function CreateInnerLink(target As String, display As String, containerPageTitle As String, htmlHelper As HtmlHelper) As String
        If target = "" Then Return target
        If target.Equals(containerPageTitle, StringComparison.InvariantCultureIgnoreCase) Then
            Return String.Format("<a class='self-link'>{0}</a>", display)
        Else
            Dim linkClass As String = IIf(WikiModel.Instance.HasPage(target), "page-link", "dead-link")
            Return htmlHelper.ActionLink(display, "FetchNamed", New With {.page = target}, New With {.title = target, .class = linkClass}).ToString()
        End If
    End Function

    Public Function CreateTaggedHereList(PageNames As IEnumerable(Of String), PageTitle As String, HtmlHelper As HtmlHelper) As String
        Dim b As New StringBuilder()
        Dim PagesTagedHere As New Dictionary(Of String, SerializableSortedSet(Of String))

        For Each name In PageNames
            Dim TaggedPages As ISet(Of String) = GetPagesByExactTag(name)
            If TaggedPages.Count > 0 Then
                PagesTagedHere.Add(name, TaggedPages)
            End If
        Next

        If PagesTagedHere.Count > 0 Then
            b.AppendLine("<h3>דפים המתויגים תחת ערך זה</h3>")
            For Each tagged In PagesTagedHere
                If PageNames.Count > 1 OrElse tagged.Key <> PageTitle Then
                    b.Append("<h4>")
                    b.Append(tagged.Key)
                    b.Append("</h4>")
                End If
                b.Append("<ul>")
                For Each taggedPage In tagged.Value
                    b.Append("<li>")
                    b.Append(WikiModel.CreateInnerLink(taggedPage, taggedPage, PageTitle, HtmlHelper))
                    b.Append("</li>")
                Next
                b.Append("</ul>")
            Next
        End If
        Return b.ToString()
    End Function
    Public Function CreateTaggedHereList(PageTitle As String, HtmlHelper As HtmlHelper) As String
        Return CreateTaggedHereList(New List(Of String) From {PageTitle}, PageTitle, HtmlHelper)
    End Function

    ' -- Classes --

    Private Class PageNameIndex

        Private Dictionary As New SerializableDictionary(Of String, SerializableSortedSet(Of String)) 'name to pages
        Private ReveseDictionary As New Dictionary(Of String, SerializableSortedSet(Of String)) 'page to it's names
        Private DataFile As FileInfo
        Private Serializer As XmlSerializer

        Private Sub Save()
            Dim xmlWriter As New XmlTextWriter(DataFile.FullName, Encoding.UTF8)
            Try
                xmlWriter.Formatting = System.Xml.Formatting.Indented
                Serializer.Serialize(xmlWriter, Dictionary)
            Finally
                xmlWriter.Close()
            End Try
        End Sub
        Private Sub Load(Optional AllowEmptyFile As Boolean = True)
            If Not DataFile.Exists() Then
                If AllowEmptyFile Then
                    Save()
                Else
                    Throw New FileNotFoundException("Page index file not found and AllowEmptyFile is set to false.", DataFile.FullName)
                End If
            End If
            If DataFile.Length = 0 Then
                If AllowEmptyFile Then
                    Save()
                Else
                    Throw New IOException("Page index file is empty and AllowEmptyFile is set to false.", DataFile.FullName)
                End If
            End If


            Dim xmlReader As New XmlTextReader(DataFile.FullName)
            Try
                Dictionary = Serializer.Deserialize(xmlReader)
                For Each Name In Dictionary.Keys
                    For Each PageTitle In Dictionary(Name)
                        If Not ReveseDictionary.ContainsKey(PageTitle) Then
                            ReveseDictionary(PageTitle) = New SerializableSortedSet(Of String)
                        End If
                        ReveseDictionary(PageTitle).Add(Name)
                    Next
                Next
            Finally
                xmlReader.Close()
            End Try
        End Sub

        Public Sub Update(WikiPage As FiledWikiPageModel)
            If WikiPage.Deleted Then
                Remove(WikiPage)
            Else
                If ReveseDictionary.ContainsKey(WikiPage.Title) Then
                    For Each oldName In ReveseDictionary(WikiPage.Title)
                        If Not WikiPage.Names.Contains(oldName) Then
                            Dictionary(oldName).Remove(WikiPage.Title)
                        End If
                    Next
                End If
                For Each newName In WikiPage.Names
                    If Not Dictionary.ContainsKey(newName) Then
                        Dictionary(newName) = New SerializableSortedSet(Of String)
                    End If
                    If Not Dictionary(newName).Contains(WikiPage.Title) Then
                        Dictionary(newName).Add(WikiPage.Title)
                    End If
                Next
                ReveseDictionary(WikiPage.Title) = New SerializableSortedSet(Of String)(WikiPage.Names)
                Save()
            End If
        End Sub

        Public Sub Remove(WikiPage As FiledWikiPageModel)
            If ReveseDictionary.ContainsKey(WikiPage.Title) Then
                For Each oldName In ReveseDictionary(WikiPage.Title)
                    Dictionary(oldName).Remove(WikiPage.Title)
                Next
                ReveseDictionary.Remove(WikiPage.Title)
                Save()
            End If
        End Sub

        Public Function HasPages(Name As String) As Boolean
            Name = StrConv(Name, VbStrConv.ProperCase)
            Return Dictionary.ContainsKey(Name) AndAlso Dictionary(Name).Count > 0
        End Function

        Public Function GetPages(Name As String) As SerializableSortedSet(Of String)
            Name = StrConv(Name, VbStrConv.ProperCase)
            If Dictionary.ContainsKey(Name) Then
                Return Dictionary(Name)
            Else
                Return New SerializableSortedSet(Of String)
            End If
        End Function

        Public Function GetAllPages() As SerializableSortedSet(Of String)
            Return New SerializableSortedSet(Of String)(ReveseDictionary.Keys)
        End Function

        Public Function GetNames(PageTitle As String) As SerializableSortedSet(Of String)
            If ReveseDictionary.ContainsKey(PageTitle) Then
                Dim l = ReveseDictionary(PageTitle)
                l.Add(PageTitle)
                Return l
            Else
                Return New SerializableSortedSet(Of String)
            End If
        End Function
        Public Sub New(DataFilePath As String)
            Me.DataFile = New FileInfo(DataFilePath)
            Me.Serializer = New XmlSerializer(Dictionary.GetType())
            Load()
        End Sub

    End Class

    Private Class PageTagsIndex

        Private Dictionary As New SerializableDictionary(Of String, SerializableSortedSet(Of String)) 'tags to pages
        Private ReveseDictionary As New Dictionary(Of String, HashSet(Of String)) 'page to it's tags
        Private Categories As New HashSet(Of String) 'pages that are categories
        Private TagsFile As FileInfo
        Private CategoriesFile As FileInfo
        Private PageNameIndex As PageNameIndex

        Private Sub Save()
            Dim DictionaryXmlWriter As New XmlTextWriter(TagsFile.FullName, Encoding.UTF8)
            Dim CategoriesXmlWriter As New XmlTextWriter(CategoriesFile.FullName, Encoding.UTF8)
            Try
                DictionaryXmlWriter.Formatting = System.Xml.Formatting.Indented
                CategoriesXmlWriter.Formatting = System.Xml.Formatting.Indented
                With New XmlSerializer(Dictionary.GetType())
                    .Serialize(DictionaryXmlWriter, Dictionary)
                End With
                With New XmlSerializer(Categories.GetType())
                    .Serialize(CategoriesXmlWriter, Categories)
                End With
            Finally
                DictionaryXmlWriter.Close()
                CategoriesXmlWriter.Close()
            End Try
        End Sub

        Private Sub Load(Optional AllowEmptyFile As Boolean = True)
            If Not TagsFile.Exists() Then
                If AllowEmptyFile Then
                    Save()
                Else
                    Throw New FileNotFoundException("Tag index file not found and AllowEmptyFile is set to false.", TagsFile.FullName)
                End If
            End If
            If TagsFile.Length = 0 Then
                If AllowEmptyFile Then
                    Save()
                Else
                    Throw New IOException("Tag index file is empty and AllowEmptyFile is set to false.", TagsFile.FullName)
                End If
            End If

            Dim DictionaryXmlReader As New XmlTextReader(TagsFile.FullName)
            Dim CategoriesXmlReader As New XmlTextReader(CategoriesFile.FullName)
            Try
                With New XmlSerializer(Dictionary.GetType())
                    Dictionary = .Deserialize(DictionaryXmlReader)
                End With
                With New XmlSerializer(Categories.GetType())
                    Categories = .Deserialize(CategoriesXmlReader)
                End With
                For Each Name In Dictionary.Keys
                    For Each PageTitle In Dictionary(Name)
                        If Not ReveseDictionary.ContainsKey(PageTitle) Then
                            ReveseDictionary(PageTitle) = New HashSet(Of String)
                        End If
                        ReveseDictionary(PageTitle).Add(Name)
                    Next
                Next
            Finally
                DictionaryXmlReader.Close()
                CategoriesXmlReader.Close()
            End Try
        End Sub

        Public Sub Update(WikiPage As FiledWikiPageModel)

            Dim NewTags As HashSet(Of String) = WikiPage.GetAllTags()

            If WikiPage.Deleted Then
                Remove(WikiPage)
            Else
                If WikiPage.IsCategory Then
                    Categories.Add(WikiPage.Title)
                ElseIf Categories.Contains(WikiPage.Title) Then
                    Categories.Remove(WikiPage.Title)
                End If

                If ReveseDictionary.ContainsKey(WikiPage.Title) Then
                    For Each oldTag In ReveseDictionary(WikiPage.Title)
                        If Not NewTags.Contains(oldTag) Then
                            Dictionary(oldTag).Remove(WikiPage.Title)
                        End If
                    Next
                End If
                For Each newTag In NewTags
                    newTag = StrConv(newTag, VbStrConv.ProperCase)
                    If Not Dictionary.ContainsKey(newTag) Then
                        Dictionary(newTag) = New SerializableSortedSet(Of String)
                    End If
                    If Not Dictionary(newTag).Contains(WikiPage.Title) Then
                        Dictionary(newTag).Add(WikiPage.Title)
                    End If
                Next
                ReveseDictionary(WikiPage.Title) = NewTags
                Save()
            End If

        End Sub

        Public Sub Remove(WikiPage As FiledWikiPageModel)
            If Categories.Contains(WikiPage.Title) Then
                Categories.Remove(WikiPage.Title)
            End If
            If ReveseDictionary.ContainsKey(WikiPage.Title) Then
                For Each oldTag In ReveseDictionary(WikiPage.Title)
                    Dictionary(oldTag).Remove(WikiPage.Title)
                Next
                ReveseDictionary.Remove(WikiPage.Title)
                Save()
            End If
        End Sub

        Public Function HasPages(Name As String) As Boolean
            Name = StrConv(Name, VbStrConv.ProperCase)
            Return Dictionary.ContainsKey(Name) AndAlso Dictionary(Name).Count > 0
        End Function

        Private Function _GetPages(Tag As String, PageList As SerializableSortedSet(Of String)) As SerializableSortedSet(Of String)

            If Not Dictionary.ContainsKey(Tag) Then
                Return PageList
            End If

            For Each Page In Dictionary(Tag)
                If Not PageList.Contains(Page) Then
                    PageList.Add(Page)
                    For Each name In PageNameIndex.GetNames(Page)
                        If Categories.Contains(name) Then
                            PageList.UnionWith(_GetPages(Page, PageList))
                        End If
                    Next
                End If
            Next

            Return PageList
        End Function
        Public Function GetPages(Tag As String, Optional FollowCategories As Boolean = True) As SerializableSortedSet(Of String)
            Tag = StrConv(Tag, VbStrConv.ProperCase)
            If Not Dictionary.ContainsKey(Tag) Then Return New SerializableSortedSet(Of String)

            If FollowCategories Then
                Return _GetPages(Tag, New SerializableSortedSet(Of String))
            Else
                Return Dictionary(Tag)
            End If
        End Function

        Public Function GetAllTags() As List(Of String)
            Return Dictionary.Keys.ToList()
        End Function

        Public Sub New(TagIndexFile As String, CategoryListFile As String, pageNameIndex As PageNameIndex)
            Me.TagsFile = New FileInfo(TagIndexFile)
            Me.CategoriesFile = New FileInfo(CategoryListFile)
            Me.PageNameIndex = pageNameIndex
            Load()
        End Sub

    End Class

    Private Class BackupManager

        Private svnClient As SvnClient
        Private gitRepo As Repository
        Private indexDir As String
        Private dbDir As String
        Private ext As String = ".xml"
        Sub New(indexDir As String, repoDir As String, dbDir As String)
            'Me.svnClient = New SvnClient()
            Me.gitRepo = New Repository(repoDir)
            Me.dbDir = dbDir
            Me.indexDir = indexDir
        End Sub

        Sub update(page As String, msg As String)
            Dim args As New SvnCommitArgs
            msg = "AutoGit - " & msg
            args.LogMessage = msg
            Dim filePath As String = Path.Combine(dbDir, page) & ext
            'Try
            'Dim info As SvnInfoEventArgs
            'svnClient.GetInfo(filePath, info)
            'Catch e As SvnWorkingCopyPathNotFoundException
            add(filePath)
            'End Try
            'svnClient.Commit({filePath, indexDir}, args)
            gitRepo.Commit(msg)
            gitRepo.Network.Push(gitRepo.Branches("Master"))
        End Sub
        Sub add(path As String)
            gitRepo.Index.Add(path)
            'svnClient.Add(filePath)
        End Sub

        '     Sub update(pagePath As String, msg As String)
        '        Dim worker As New Threading.Thread(Sub() _update(pagePath, msg))
        '        worker.Start()
        '    End Sub
        '   Sub add(pagePath As String, msg As String)
        '      Dim worker As New Threading.Thread(Sub() _add(pagePath, msg))
        '      worker.Start()
        '  End Sub

        Sub move(oldName As String, targetName As String)
            svnClient.Move(Path.Combine(dbDir, oldName) & ext, Path.Combine(dbDir, targetName))
        End Sub

        'Sub delete(page As String)
        '     svnClient.Delete(Path.Combine(dbDir, page))
        ' End Sub
    End Class


    Private Class PagesDataBase

        Private Class DynamicXmlSerializer
            Public Sub Serialize(instance As FiledWikiPageModel, filePath As String)
                Dim serializer As New XmlSerializer(instance.GetType())
                Dim xmlWriter As New XmlTextWriter(filePath, Encoding.UTF8)
                xmlWriter.Formatting = System.Xml.Formatting.Indented
                Try
                    serializer.Serialize(xmlWriter, instance)
                Finally
                    xmlWriter.Close()
                End Try
            End Sub

            Public Function Deserialize(filePath As String) As FiledWikiPageModel
                Dim xmlReader As New XmlTextReader(filePath)
                xmlReader.MoveToContent()
                Dim serializer As New XmlSerializer(GetType(FiledWikiPageModel))
                Try
                    Return serializer.Deserialize(xmlReader)
                Finally
                    xmlReader.Close()
                End Try
            End Function
        End Class

        Public Class PagesDataBaseException
            Inherits WikiException
            Sub New(Msg As String)
                MyBase.New(Msg)
            End Sub
        End Class
        Public Class PageNotFoundException
            Inherits PagesDataBaseException
            Sub New(PageName As String)
                MyBase.New(Strings.Format("No page named '{0}'", PageName))
            End Sub
        End Class

        Private Const PageFileExtension = ".xml"
        Private serializer As New DynamicXmlSerializer()
        Private Property DbFolder As DirectoryInfo

        Public ReadOnly Property DatabaseFolderPath As String
            Get
                Return DbFolder.FullName
            End Get
        End Property

        Public Sub SavePage(page As FiledWikiPageModel)
            serializer.Serialize(page, Path.Combine(DbFolder.FullName, page.Title & PageFileExtension))
        End Sub

        Public Sub New(DbFolderPath As String)
            Me.DbFolder = New DirectoryInfo(DbFolderPath)
        End Sub

        Public Function GetPage(PageTitle As String) As FiledWikiPageModel
            Return serializer.Deserialize(GetPageFile(PageTitle).FullName)
        End Function
        Public Function GetPage(PageFile As FileInfo) As FiledWikiPageModel
            Return serializer.Deserialize(PageFile.FullName)
        End Function

        Public Function GetPageFile(PageTitle As String) As FileInfo
            Dim Results As FileInfo() = DbFolder.GetFiles(PageTitle & PageFileExtension)
            If Results.Length = 0 Then
                Throw New PageNotFoundException(PageTitle)
            ElseIf Results.Length <> 1 Then
                Throw New PagesDataBaseException("Multiple files of same name?! - Likely a bug.")
            Else
                Return Results(0)
            End If
        End Function

        Sub DeletePage(PageTitle As String)
            Dim Results As FileInfo() = DbFolder.GetFiles(PageTitle & PageFileExtension)
            If Results.Length = 0 Then
                Throw New PageNotFoundException(PageTitle)
            ElseIf Results.Length <> 1 Then
                Throw New PagesDataBaseException("Multiple files of same name?! - Likely a bug.")
            Else
                Results(0).Delete()
            End If
        End Sub

        Function HasPage(PageTitle As String) As Boolean
            Return DbFolder.GetFiles(PageTitle & PageFileExtension).Length > 0
        End Function

        Private Class _ChangeDateComparer
            Implements IComparer(Of IO.FileSystemInfo)
            Public Function Compare(x As FileSystemInfo, y As FileSystemInfo) As Integer Implements IComparer(Of FileSystemInfo).Compare
                Return y.LastWriteTime.CompareTo(x.LastWriteTime) 'y is compared to x in order to reverse it to last modiefied being first
            End Function
        End Class
        Function GetRecentlyChangedPages(Count As Integer) As List(Of String)
            Dim infos As FileSystemInfo() = DbFolder.GetFileSystemInfos()
            Array.Sort(infos, New _ChangeDateComparer())
            Dim pages As New List(Of String)
            For i = 0 To Count - 1
                pages.Add(Path.GetFileNameWithoutExtension(infos(i).Name))
            Next
            Return pages
        End Function

    End Class

    Private Class PagesArchiver

        Private Const PageFileExtension = ".xml"
        Private Property PageDb As PagesDataBase
        Private Property ArchiveFolder As DirectoryInfo

        Public Sub New(ArchiveFolderPath As String, PageDb As PagesDataBase)
            Me.ArchiveFolder = New DirectoryInfo(ArchiveFolderPath)
            Me.PageDb = PageDb
        End Sub

        Public Sub ArchivePage(PageTitle As String)
            Dim PageFile As FileInfo = PageDb.GetPageFile(PageTitle)
            Dim Page As FiledWikiPageModel = PageDb.GetPage(PageFile)

            Dim newFilePathBuilder As New StringBuilder
            newFilePathBuilder.Append(ArchiveFolder.FullName)
            newFilePathBuilder.Append(Path.DirectorySeparatorChar)
            newFilePathBuilder.Append(PageTitle)
            newFilePathBuilder.Append(".")
            newFilePathBuilder.Append(Page.GetVersionCode())
            newFilePathBuilder.Append(PageFileExtension)

            PageFile.CopyTo(newFilePathBuilder.ToString) 'We copy to let the svn delete the file
        End Sub

        Public Function ListVersionsDates(PageTitle As String) As List(Of Date)
            Dim files = ArchiveFolder.GetFiles(String.Format("{0}.*{1}", PageTitle, PageFileExtension))
            Dim regex As New Regex(String.Format(("{0}.(\d+){1}"), PageTitle, PageFileExtension))
            Dim versions As New List(Of Date)
            Dim match As Match
            For Each f In files
                match = regex.Match(f.Name)
                If match.Success Then
                    versions.Add(New Date(CLng(match.Groups(1).Value) * TimeSpan.TicksPerSecond))
                End If
            Next
            Return versions
        End Function
        Public Function GetVersions(PageTitle As String) As List(Of FiledWikiPageModel)
            Dim files = ArchiveFolder.GetFiles(String.Format("{0}.*{1}", PageTitle, PageFileExtension))
            Dim regex As New Regex(String.Format(("{0}.(\d+){1}"), PageTitle, PageFileExtension))
            Dim versions As New List(Of FiledWikiPageModel)
            Dim match As Match
            For Each f In files
                match = regex.Match(f.Name)
                If match.Success Then
                    Dim p As FiledWikiPageModel = PageDb.GetPage(f)
                    p.IsArchivedVersion = True
                    versions.Add(p)
                End If
            Next
            Return versions
        End Function

        Public Function GetPage(Page As String, Version As Long) As FiledWikiPageModel
            Try
                Dim FileName As String = String.Format("{0}.{1}{2}", Page, Version, PageFileExtension)
                Dim f As FileInfo = ArchiveFolder.GetFiles(FileName).Single()
                Dim p = PageDb.GetPage(f)
                p.IsArchivedVersion = True
                Return p
            Catch e As InvalidOperationException
                Throw New WikiNoPageVersionException(Page, Version)
            End Try
        End Function

        Sub DeleteFromArchive(PageTitle As String)
            Dim files = ArchiveFolder.GetFiles(String.Format("{0}.*{1}", PageTitle, PageFileExtension))
            Dim regex As New Regex(String.Format(("{0}.(\d+){1}"), PageTitle, PageFileExtension))
            Dim versions As New List(Of FiledWikiPageModel)
            Dim match As Match
            For Each f In files
                match = regex.Match(f.Name)
                If match.Success Then
                    f.Delete()
                End If
            Next
        End Sub

    End Class


    Private Class SearchIndexer

        Private HootIndexer As hOOt.Hoot
        Private pageDb As PagesDataBase
        Public Sub New(indexFolder As String, pageDb As PagesDataBase)
            Me.HootIndexer = New hOOt.Hoot(indexFolder, "SearchIndex", True)
            Me.pageDb = pageDb
        End Sub

        Public Sub UpdatePage(page As FiledWikiPageModel)
            HootIndexer.IndexUpdate(pageDb.GetPageFile(page.Title), page.Content)
            HootIndexer.OptimizeIndex()
            HootIndexer.Save()
        End Sub
        Public Sub RemovePage(page As FiledWikiPageModel)
            HootIndexer.RemoveDocument(pageDb.GetPageFile(page.Title).FullName)
            HootIndexer.OptimizeIndex()
            HootIndexer.Save()
        End Sub

        Public Function Search(query As String, Optional FlippedQuery As String = Nothing) As SearchResults
            query = Trim(query)
            Dim res As SearchResults
            If FlippedQuery Is Nothing Then
                'Dim fixedQuery As String = Regex.Replace(query, "[" + Regex.Escape(New String(InvalidChars)) + "]", "")
                res = New SearchResults(query)
                If Instance.HasPage(query) Then res.setMatched()
                res.InvalidChars = Regex.IsMatch(query, "[" + Regex.Escape(New String(InvalidChars)) + "]")
            Else
                'Dim fixedFlippedQuery As String = Regex.Replace(FlippedQuery, Regex.Escape(New String(InvalidChars)), "")
                res = New SearchResults(query, FlippedQuery)
                If Instance.HasPage(FlippedQuery) Then res.setMatched()
                res.InvalidChars = Regex.IsMatch(FlippedQuery, "[" + Regex.Escape(New String(InvalidChars)) + "]")
            End If



            For Each doc In HootIndexer.FindDocumentFileNames(query)
                res.AddPage(Path.GetFileNameWithoutExtension(doc))
            Next

            If FlippedQuery Is Nothing AndAlso res.Pages.Count = 0 AndAlso Not res.TitleMatch AndAlso IsLatin(query) Then
                Return Search(query, FlipKeyboard(query))
            End If
            Return res
        End Function
    End Class
    Private Shared Function IsLatin(str As String) As Boolean
        For Each c In str
            If c < " "c Or c > "~"c Then Return False
        Next
        Return True
    End Function
    Private Shared Function FlipKeyboard(str As String) As String
        Dim d As New Dictionary(Of Char, Char) From {
            {"q"c, "/"c},
            {"w"c, "'"c},
            {"e"c, "ק"c},
            {"r"c, "ר"c},
            {"t"c, "א"c},
            {"y"c, "ט"c},
            {"u"c, "ו"c},
            {"i"c, "ן"c},
            {"o"c, "ם"c},
            {"p"c, "פ"c},
            {"a"c, "ש"c},
            {"s"c, "ד"c},
            {"d"c, "ג"c},
            {"f"c, "כ"c},
            {"g"c, "ע"c},
            {"h"c, "י"c},
            {"j"c, "ח"c},
            {"k"c, "ל"c},
            {"l"c, "ך"c},
            {";"c, "ף"c},
            {"'"c, ","c},
            {"z"c, "ז"c},
            {"x"c, "ס"c},
            {"c"c, "ב"c},
            {"v"c, "ה"c},
            {"b"c, "נ"c},
            {"n"c, "מ"c},
            {"m"c, "צ"c},
            {","c, "ת"c},
            {"."c, "ץ"c},
            {"/"c, "."c}
        }
        Dim b As New StringBuilder(str.Length)
        For Each c In str
            c = LCase(c)
            If d.ContainsKey(c) Then c = d(c)
            b.Append(c)
        Next
        Return b.ToString()
    End Function

End Class
